/*
 * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
 *
 * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
 *
 * Spydroid is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This source code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package net.majorkernelpanic.streaming;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Random;

import net.majorkernelpanic.spydroid.SpydroidApplication;
import net.majorkernelpanic.streaming.audio.AudioStream;
import net.majorkernelpanic.streaming.rtp.AbstractPacketizer;
import net.majorkernelpanic.streaming.video.VideoStream;

import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.util.Log;

/**
 * A MediaRecorder that streams what it records using a packetizer from the rtp package.
 * You can't use this class directly !
 */
public abstract class MediaStream implements Stream {

    protected static final String TAG = "MediaStream";

    /**
     * Raw audio/video will be encoded using the MediaRecorder API.
     */
    public static final byte MODE_MEDIARECORDER_API = 0x01;

    /**
     * Raw audio/video will be encoded using the MediaCodec API with buffers.
     */
    public static final byte MODE_MEDIACODEC_API = 0x02;

    /**
     * Raw audio/video will be encoded using the MediaCode API with a surface.
     */
    public static final byte MODE_MEDIACODEC_API_2 = 0x05;

    /**
     * Prefix that will be used for all shared preferences saved by libstreaming
     */
    protected static final String PREF_PREFIX = "libstreaming-";

    /**
     * The packetizer that will read the output of the camera and send RTP packets over the networkd.
     */
    protected AbstractPacketizer mPacketizer = null;

    protected static byte sSuggestedMode;
    protected byte mMode, mRequestedMode;

    protected boolean mStreaming = false, mConfigured = false;
    protected int mRtpPort = 0, mRtcpPort = 0;
    protected InetAddress mDestination;
    protected LocalSocket mReceiver, mSender = null;
    private LocalServerSocket mLss = null;
    private int mSocketId, mTTL = 64;

    protected MediaRecorder mMediaRecorder;
    protected MediaCodec mMediaCodec;

    /**
     * RTP视频数据经过tcp传递
     */
    public static final int RTP_OVER_TCP = 1 << 1;
    /**
     * RTP视频数据经过udp传递
     */
    public static final int RTP_OVER_UDP = 1 << 2;

    /**
     * RTP层当中视频流的传输通道
     * 目前RTP层当中视频流的传输有两种方式，TCP和UDP.
     * 如果用户不指定该值的话，默认就采用udp的方式进行传输.
     */
    protected int mStreamDataTransferChannel;

    static {
        // We determine whether or not the MediaCodec API should be used
        try {
            // 优先使用MediaCodeC(部分原因应该是MediaCodeC使用的是硬编码)
            Class.forName("android.media.MediaCodec");
            // Will be set to MODE_MEDIACODEC_API at some point...
            sSuggestedMode = MODE_MEDIACODEC_API;
            Log.i(TAG, "Phone supports the MediaCoded API");
        } catch (ClassNotFoundException e) {
            sSuggestedMode = MODE_MEDIARECORDER_API;
            Log.i(TAG, "Phone does not support the MediaCodec API");
        }
    }

    public MediaStream() {
        mRequestedMode = sSuggestedMode;
        mMode = sSuggestedMode;
    }

    /**
     * Sets the destination ip address of the stream.
     *
     * @param dest The destination address of the stream
     */
    public void setDestinationAddress(InetAddress dest) {
        mDestination = dest;
    }

    /**
     * Sets the destination ports of the stream.
     * If an odd number is supplied for the destination port then the next
     * lower even number will be used for RTP and it will be used for RTCP.
     * If an even number is supplied, it will be used for RTP and the next odd
     * number will be used for RTCP.
     *
     * @param dport The destination port
     */
    public void setDestinationPorts(int dport) {
        if (dport % 2 == 1) {
            mRtpPort = dport - 1;
            mRtcpPort = dport;
        } else {
            mRtpPort = dport;
            mRtcpPort = dport + 1;
        }
    }

    /**
     * Sets the destination ports of the stream.
     *
     * @param rtpPort  Destination port that will be used for RTP
     * @param rtcpPort Destination port that will be used for RTCP
     */
    public void setDestinationPorts(int rtpPort, int rtcpPort) {
        mRtpPort = rtpPort;
        mRtcpPort = rtcpPort;
    }

    /**
     * Sets the Time To Live of packets sent over the network.
     *
     * @param ttl The time to live
     * @throws IOException
     */
    public void setTimeToLive(int ttl) throws IOException {
        mTTL = ttl;
    }

    /**
     * Returns a pair of destination ports, the first one is the
     * one used for RTP and the second one is used for RTCP.
     **/
    public int[] getDestinationPorts() {
        return new int[]{
                mRtpPort,
                mRtcpPort
        };
    }

    /**
     * Returns a pair of source ports, the first one is the
     * one used for RTP and the second one is used for RTCP.
     **/
    public int[] getLocalPorts() {
        return new int[]{
                this.mPacketizer.getRtpSocket().getLocalPort(),
                this.mPacketizer.getRtcpSocket().getLocalPort()
        };
    }

    /**
     * Sets the streaming method that will be used.
     * <p>
     * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded
     * using the MediaRecorder API. <br />
     * <p>
     * If the mode is set to {@link #MODE_MEDIACODEC_API} or to {@link #MODE_MEDIACODEC_API_2},
     * audio/video will be encoded with using the MediaCodec. <br />
     * <p>
     * The {@link #MODE_MEDIACODEC_API_2} mode only concerns {@link VideoStream}, it makes
     * use of the createInputSurface() method of the MediaCodec API (Android 4.3 is needed there). <br />
     *
     * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API} or {@link #MODE_MEDIACODEC_API_2}
     */
    public void setStreamingMethod(byte mode) {
        mRequestedMode = mode;
    }

    /**
     * Returns the packetizer associated with the {@link MediaStream}.
     *
     * @return The packetizer
     */
    public AbstractPacketizer getPacketizer() {
        return mPacketizer;
    }

    /**
     * Returns an approximation of the bit rate consumed by the stream in bit per seconde.
     */
    public long getBitrate() {
        return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate();
    }

    /**
     * Indicates if the {@link MediaStream} is streaming.
     *
     * @return A boolean indicating if the {@link MediaStream} is streaming
     */
    public boolean isStreaming() {
        return mStreaming;
    }

    /**
     * Configures the stream with the settings supplied with
     * {@link VideoStream#setVideoQuality(net.majorkernelpanic.streaming.video.VideoQuality)}
     * for a {@link VideoStream} and {@link AudioStream#setAudioQuality(net.majorkernelpanic.streaming.audio.AudioQuality)}
     * for a {@link AudioStream}.
     */
    @Override
    public synchronized void configure() throws IllegalStateException, IOException {
        if (mStreaming) {
            throw new IllegalStateException("Can't be called while streaming.");
        }
        mMode = mRequestedMode;
        mConfigured = true;
    }

    /**
     * Starts the stream.
     */
    @Override
    public synchronized void start() throws IllegalStateException, IOException {
        if (mDestination == null) {
            throw new IllegalStateException("No destination ip address set for the stream !");
        }

        if (mRtpPort <= 0 || mRtcpPort <= 0) {
            throw new IllegalStateException("No destination ports set for the stream !");
        }

        Log.d(TAG, "transfer video data with channel of " + mStreamDataTransferChannel);

        mPacketizer.setTimeToLive(mTTL);

        if (mMode != MODE_MEDIARECORDER_API) {
            Log.v(TAG, "Starts Streaming encode with MediaCodeC");
            encodeWithMediaCodec();
        } else {
            Log.v(TAG, "Starts Streaming encode with MediaRecorder");
            encodeWithMediaRecorder();
        }
    }

    @Override
    public void setTransferChannel(int transferChannel) {
        this.mStreamDataTransferChannel = transferChannel;
    }

    /**
     * Stops the stream.
     */
    @SuppressLint("NewApi")
    public synchronized void stop() {
        if (mStreaming) {
            try {
                if (SpydroidApplication.USE_SHARE_BUFFER_DATA) {
                    Log.d(TAG, "stop the Packetizer and stop the MediaCodec");
                    // 如果是从ShareBuffer当中读取数据时,我们只需要将
                    // socket关闭,Packetizer关闭
                    mPacketizer.stop();
                    mMediaCodec.stop();
                    mMediaCodec.release();
                    mMediaCodec = null;
                } else {
                    if (mMode == MODE_MEDIARECORDER_API) {
                        mMediaRecorder.stop();
                        mMediaRecorder.release();
                        mMediaRecorder = null;
                        closeSockets();
                        mPacketizer.stop();
                    } else {
                        mPacketizer.stop();
                        mMediaCodec.stop();
                        mMediaCodec.release();
                        mMediaCodec = null;
                    }
                }
            } catch (final Exception e) {
                Log.e(TAG, "Exception happened while we stop the stream", e);
            }
            mStreaming = false;
        }
    }

    protected abstract void encodeWithMediaRecorder() throws IOException;

    protected abstract void encodeWithMediaCodec() throws IOException;

    /**
     * Returns a description of the stream using SDP.
     * This method can only be called after {@link Stream#configure()}.
     *
     * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called.
     */
    public abstract String getSessionDescription();

    /**
     * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}.
     *
     * @return the SSRC of the stream
     */
    public int getSSRC() {
        return getPacketizer().getSSRC();
    }

    protected void createSockets() throws IOException {
        final String LOCAL_ADDR = "net.majorkernelpanic.streaming-";

        for (int i = 0; i < 10; i++) {
            try {
                mSocketId = new Random().nextInt();
                mLss = new LocalServerSocket(LOCAL_ADDR + mSocketId);
                break;
            } catch (IOException e) {
                Log.e(TAG, "IOException happened while create the socket", e);
            }
        }

        mReceiver = new LocalSocket();
        mReceiver.connect(new LocalSocketAddress(LOCAL_ADDR + mSocketId));
        mReceiver.setReceiveBufferSize(500000);
        mReceiver.setSoTimeout(3000);
        mSender = mLss.accept();
        mSender.setSendBufferSize(500000);
    }

    protected void closeSockets() {
        try {
            mReceiver.close();
        } catch (Exception e) {
            Log.e(TAG, "fail to close the receiver", e);
        }
        try {
            mSender.close();
        } catch (Exception e) {
            Log.e(TAG, "fail to close the sender", e);
        }
        try {
            mLss.close();
        } catch (Exception e) {
            Log.e(TAG, "fail to close the LocalServerSocket", e);
        }
        mLss = null;
        mSender = null;
        mReceiver = null;
    }
}
